---
title: 액션
description: 어디서나 호출할 수 있는 타입이 안전한 서버 함수를 만드는 방법을 알아보세요.
i18nReady: true
---

import { Steps } from '@astrojs/starlight/components';
import Since from '~/components/Since.astro';
import ReadMore from '~/components/ReadMore.astro';

<p><Since v="4.15" /></p>

Astro 액션을 사용하면 타입 안전성을 갖춘 백엔드 함수를 정의하고 호출할 수 있습니다. 액션은 데이터 가져오기, JSON 구문 분석, 입력 유효성 검사를 수행합니다. 이렇게 하면 [API 엔드포인트](/ko/guides/endpoints/)를 사용할 때보다 필요한 상용구의 양을 크게 줄일 수 있습니다.

클라이언트와 서버 코드 간의 원활한 통신을 위해 API 엔드포인트 대신 액션을 사용하세요:

- [Zod 유효성 검사](https://zod.dev/?id=primitives)를 사용하여 JSON 및 양식 데이터 입력의 유효성을 자동으로 검사하세요.
- 클라이언트는 물론 [HTML 양식 액션에서도](#html-양식-액션에서-액션-호출) 백엔드를 호출할 수 있는 타입이 안전한 함수를 생성하세요. 수동 `fetch()` 호출이 필요 없습니다.
- [`ActionError`](/ko/reference/modules/astro-actions/#actionerror) 객체로 백엔드 오류를 표준화하세요.

## 기본 사용법

액션은 `src/actions/index.ts`에서 내보낸 `server` 객체에 정의됩니다:

```ts title="src/actions/index.ts"
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';

export const server = {
  myAction: defineAction({ /* ... */ })
}
```

액션은 `astro:actions` 모듈에서 함수로 사용할 수 있습니다. `actions`를 가져와서 [UI 프레임워크 컴포넌트](/ko/guides/framework-components/), [양식 POST 요청](#html-양식-액션에서-액션-호출) 또는 Astro 컴포넌트에서 `<script>` 태그를 사용하여 클라이언트 측에서 호출합니다.

액션을 호출하면 JSON 직렬화된 결과가 포함된 `data` 또는 발생한 오류가 포함된 `error`가 포함된 객체를 반환합니다.

```astro title="src/pages/index.astro"
---
---

<script>
import { actions } from 'astro:actions';

async () => {
  const { data, error } = await actions.myAction({ /* ... */ });
}
</script>
```

### 첫 번째 액션 작성하기

다음 단계에 따라 액션을 정의하고 Astro 페이지의 `script` 태그에서 호출합니다.

<Steps>

1. `src/actions/index.ts` 파일을 만들고 `server` 객체를 내보냅니다.

    ```ts title="src/actions/index.ts"
    export const server = {
      // 액션 정의 
    }
    ```

2. `astro:actions`에서 `defineAction()` 유틸리티를, `astro:schema`에서 `z` 객체를 가져옵니다.

    ```ts ins={1-2} title="src/actions/index.ts"
    import { defineAction } from 'astro:actions';
    import { z } from 'astro:schema';

    export const server = {
      // 액션 정의 
    }
    ```

3. `defineAction()` 유틸리티를 사용하여 `getGreeting` 액션을 정의합니다. `input` 속성은 [Zod](https://zod.dev) 스키마로 입력 매개변수의 유효성을 검사하는 데 사용되며 `handler()` 함수에는 서버에서 실행할 백엔드 로직이 포함되어 있습니다.

    ```ts ins={5-12} title="src/actions/index.ts"
    import { defineAction } from 'astro:actions';
    import { z } from 'astro:schema';

    export const server = {
      getGreeting: defineAction({
        input: z.object({
          name: z.string(),
        }),
        handler: async (input) => {
          return `Hello, ${input.name}!`
        }
      })
    }
    ```

4. 클릭 시 `getGreeting` 액션을 사용하여 인사말을 가져오는 버튼이 있는 Astro 컴포넌트를 만듭니다.

    ```astro title="src/pages/index.astro"
    ---
    ---

    <button>Get greeting</button>

    <script>
    const button = document.querySelector('button');
    button?.addEventListener('click', async () => {
      // 액션 인사말이 포함된 알림 팝업 표시
    });
    </script>
    ```

5. 액션을 사용하려면 `astro:actions`에서 `actions`를 가져온 다음, 클릭 핸들러에서 `actions.getGreeting()`을 호출합니다. `name` 옵션이 서버의 액션의 `handler()`로 전송되며, 오류가 없는 경우 `data` 속성으로 결과를 사용할 수 있습니다.

    ```astro title="src/pages/index.astro" ins={7, 12-13}
    ---
    ---

    <button>Get greeting</button>

    <script>
    import { actions } from 'astro:actions';

    const button = document.querySelector('button');
    button?.addEventListener('click', async () => {
      // 액션 인사말이 포함된 알림 팝업 표시
      const { data, error } = await actions.getGreeting({ name: "Houston" });
      if (!error) alert(data);
    })
    </script>
    ```

</Steps>

<ReadMore>[`defineAction()`](/ko/reference/modules/astro-actions/#defineaction) 및 해당 속성에 대한 자세한 내용은 전체 액션 API 설명서를 참조하세요.</ReadMore>

## 액션 조직화

프로젝트의 모든 액션은 `src/actions/index.ts` 파일의 `server` 객체에서 내보내져야 합니다. 액션을 인라인으로 정의하거나 액션 정의를 별도의 파일로 이동하여 가져올 수 있습니다. 중첩된 객체에서 관련 함수를 그룹화할 수도 있습니다.

예를 들어 모든 사용자 액션을 한 곳에 배치하려면 `src/actions/user.ts` 파일을 만들고 단일 `user` 객체 안에 `getUser`와 `createUser`의 정의를 모두 중첩하면 됩니다.

```ts title="src/actions/user.ts"
import { defineAction } from 'astro:actions';

export const user = {
  getUser: defineAction(/* ... */),
  createUser: defineAction(/* ... */),
}
```

그런 다음 이 `user` 객체를 `src/actions/index.ts` 파일로 가져와 다른 액션과 함께 `server` 객체에 최상위 키로 추가할 수 있습니다:

```ts title="src/actions/index.ts" ins={1,5}
import { user } from './user';

export const server = {
  myAction: defineAction({ /* ... */ }),
  user,
}
```

이제 모든 사용자 액션을 `actions.user` 객체에서 호출할 수 있습니다:

- `actions.user.getUser()`
- `actions.user.createUser()`


## 반환된 데이터 처리

액션은 `handler()`의 타입이 안전한 반환값이 포함된 `data` 또는 백엔드 오류가 있는 `error`를 포함하는 객체를 반환합니다. 오류는 `input` 속성의 유효성 검사 오류 또는 `handler()`에서 발생한 오류로 인해 발생할 수 있습니다.

액션은 [Devalue 라이브러리를 사용하여](https://github.com/Rich-Harris/devalue) Date, Map, Set, URL을 처리할 수 있는 사용자 정의 데이터 형식을 반환합니다. 따라서 일반 JSON처럼 네트워크에서 응답을 쉽게 검사할 수 없습니다. 대신 디버깅을 위해 액션이 반환하는 `data` 객체를 검사할 수 있습니다.

<ReadMore>자세한 내용은 [`handler()` API 참조를 확인하세요](/ko/reference/modules/astro-actions/#handler-속성).</ReadMore>

### 오류 확인

`data` 속성을 사용하기 전에 `error`가 있는지 확인하는 것이 가장 좋습니다. 이렇게 하면 오류를 미리 처리할 수 있고 `data`가 `undefined`인지 확인하지 않고 정의되도록 할 수 있습니다.

```ts
const { data, error } = await actions.example();

if (error) {
  // 오류 처리 케이스
  return;
}
// `data` 사용 
```

### 오류 확인 없이 `data`에 직접 접근하기

예를 들어 프로토타입을 만들거나 오류를 잡아주는 라이브러리를 사용할 때, 오류 처리를 건너뛰려면 액션 호출에 `.orThrow()` 속성을 사용하여 `error`를 반환하는 대신 오류를 던지세요. 그러면 액션의 `data`가 직접 반환됩니다.

이 예시에서는 `handler` 액션에서 업데이트된 '좋아요' 수를 `number`로 반환하는 `likePost()` 액션을 호출합니다:

```ts ins="orThrow"
const updatedLikes = await actions.likePost.orThrow({ postId: 'example' });
//    ^ type: number
```

### 액션에서 백엔드 오류 처리

제공된 `ActionError`를 사용하여 액션 `handler()`에서 데이터베이스 항목이 누락된 경우 "찾을 수 없음", 사용자가 로그인하지 않은 경우 "권한 없음"과 같은 오류를 발생시킬 수 있습니다. 이는 `undefined`를 반환하는 것보다 두 가지 주요 이점이 있습니다:


- `404 - Not found` 또는 `401 - Unauthorized`와 같은 상태 코드를 설정할 수 있습니다. 이렇게 하면 각 요청의 상태 코드를 확인할 수 있어 개발과 프로덕션 모두에서 디버깅 오류를 개선할 수 있습니다.

- 애플리케이션 코드에서 모든 오류는 액션 결과의 `error` 객체에 전달됩니다. 이렇게 하면 데이터가 `undefined`인지 검사를 할 필요가 없으며, 무엇이 잘못되었는지에 따라 사용자에게 맞춤형 피드백을 표시할 수 있습니다.

#### `ActionError` 만들기

오류를 발생시키려면 `astro:actions` 모듈에서 [`ActionError()` 클래스](/ko/reference/modules/astro-actions/#actionerror)를 가져옵니다. 사람이 읽을 수 있는 상태 `code` (예: `"NOT_FOUND"` 또는 `"BAD_REQUEST"`)와 오류에 대한 추가 정보를 제공하기 위한 선택적 `message`를 전달합니다.

이 예시에서는 사용자가 로그인하지 않은 경우, 가상의 "user-session" 쿠키를 확인하여 인증을 수행한 후 `likePost` 액션에서 오류를 발생시킵니다:

```ts title="src/actions/index.ts" ins=/ActionError(?= )/ ins={9-12}
import { defineAction, ActionError } from "astro:actions";
import { z } from "astro:schema";

export const server = {
  likePost: defineAction({
    input: z.object({ postId: z.string() }),
    handler: async (input, ctx) => {
      if (!ctx.cookies.has('user-session')) {
        throw new ActionError({
          code: "UNAUTHORIZED",
          message: "User must be logged in.",
        });
      }
      // 그렇지 않으면, 게시글에 좋아요를 추가합니다.
    },
  }),
};
```

#### `ActionError` 처리하기

이 오류를 처리하려면 애플리케이션에서 액션을 호출하고 `error` 프로퍼티가 있는지 확인할 수 있습니다. 이 프로퍼티는 `ActionError` 타입이며 `code`와 `message`를 포함합니다.

다음 예시에서 `LikeButton.tsx` 컴포넌트를 클릭하면 `likePost()` 액션이 호출됩니다. 인증 오류가 발생하면 `error.code` 속성을 사용하여 로그인 링크를 표시할지 여부를 결정합니다:

```tsx title=src/components/LikeButton.tsx ins="if (error?.code === 'UNAUTHORIZED') setShowLogin(true);"
import { actions } from 'astro:actions';
import { useState } from 'preact/hooks';

export function LikeButton({ postId }: { postId: string }) {
  const [showLogin, setShowLogin] = useState(false);
  return (
    <>
      {
        showLogin && <a href="/signin">Log in to like a post.</a>
      }
      <button onClick={async () => {
        const { data, error } = await actions.likePost({ postId });
        if (error?.code === 'UNAUTHORIZED') setShowLogin(true);
        // 예상치 못한 오류에 대해 조기 반환을 수행합니다.
        else if (error) return;
        // 좋아요 수 업데이트
      }}>
        Like
      </button>
    </>
  )
}
```

### 클라이언트 리디렉션 처리하기 

클라이언트에서 액션을 호출할 때 `react-router`와 같은 클라이언트 측 라이브러리와 통합하거나, 액션이 성공하면 새 페이지로 리디렉션하는 Astro의 [`navigate()` 함수](/ko/guides/view-transitions/#탐색-트리거)를 사용할 수 있습니다.

이 예시는 `logout` 액션이 성공적으로 반환된 후 홈페이지로 이동합니다:

```tsx title=src/pages/LogoutButton.tsx {2,7-8}
import { actions } from 'astro:actions';
import { navigate } from 'astro:transitions/client';

export function LogoutButton() {
  return (
    <button onClick={async () => {
      const { error } = await actions.logout();
      if (!error) navigate('/');
    }}>
      Logout
    </button>
  );
}
```

## 액션에서 양식 데이터 수신하기 

액션은 기본적으로 JSON 데이터를 수신합니다. HTML 양식의 양식 데이터를 수신하려면 `defineAction()` 호출에서 `accept: 'form'`을 설정하세요:

```ts title="src/actions/index.ts" ins={6}
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';

export const server = {
  comment: defineAction({
    accept: 'form',
    input: z.object(/* ... */),
    handler: async (input) => { /* ... */ },
  })
}
```

### 양식 입력에서 유효성 검사하기

액션이 [양식 데이터를 허용하도록 구성된](/ko/reference/modules/astro-actions/#accept-속성) 경우, 필드의 유효성을 검사하기 위해 모든 Zod 유효성 검사기를 사용할 수 있습니다. (예: 날짜 입력의 경우 `z.coerce.date()`) `.refine()`, `.transform()`, `.pipe()`를 포함한 확장 함수도 `z.object()` 유효성 검사기에서 지원됩니다.

또한 Astro는 편의를 위해 다음과 같은 유형의 필드 입력에 대한 유효성 검사를 내부적으로 특별히 처리합니다.

- `number` 타입의 입력은 `z.number()`를 사용하여 검사할 수 있습니다.
- `checkbox` 타입의 입력은 `z.coerce.boolean()`을 사용하여 검사할 수 있습니다.
- `file` 타입의 입력은 `z.instanceof(File)`을 사용하여 검사할 수 있습니다.
- 동일한 `name`을 가진 여러 입력은 `z.array(/* 유효성 검사기 */)`를 사용하여 검사할 수 있습니다.
- 다른 모든 입력은 `z.string()`을 사용하여 검사할 수 있습니다.

양식을 빈 입력으로 제출하면, 출력 타입이 `input` 유효성 검사기와 일치하지 않을 수 있습니다. 배열이나 불리언을 검사하는 경우가 아니라면 빈 값은 `null`로 변환됩니다. 예를 들어, `text` 타입의 입력을 빈 값으로 제출하면 결과는 빈 문자열(`""`) 대신 `null`이 됩니다.

다양한 유효성 검사기의 합집합을 적용하려면 `z.discriminatedUnion()` 래퍼를 사용하여 특정 양식 필드를 기반으로 타입을 좁히세요. 다음은 사용자를 "생성"하거나 "업데이트"하기 위해 양식 제출을 허용하며, `type`이라는 이름의 양식 필드를 사용하여 어떤 객체에 유효성 검사를 수행할지 결정하는 예시입니다.

```ts title="src/actions/index.ts" {7-21} "create" "update"
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';

export const server = {
  changeUser: defineAction({
    accept: 'form',
    input: z.discriminatedUnion('type', [
      z.object({
        // `type` 필드의 값이 `create`인 경우 일치합니다.
        type: z.literal('create'),
        name: z.string(),
        email: z.string().email(),
      }),
      z.object({
        // `type` 필드의 값이 `update`인 경우 일치합니다.
        type: z.literal('update'),
        id: z.number(),
        name: z.string(),
        email: z.string().email(),
      }),
    ]),
    async handler(input) {
      if (input.type === 'create') {
        // input은 { type: 'create', name: string, email: string }입니다.
      } else {
        // input은 { type: 'update', id: number, name: string, email: string }입니다.
      }
    },
  }),
};
```

### 양식 데이터 검증하기

액션은 각 입력의 `name` 속성 값을 객체 키로 사용하여 제출된 양식 데이터를 객체로 구문 분석합니다. 예를 들어, `<input name="search">`이 포함된 양식은 `{ search: 'user input' }`과 같이 객체로 구문 분석됩니다. 액션의 `input` 스키마는 이 객체의 유효성을 검사하는 데 사용됩니다.

액션 핸들러에서 구문 분석된 객체 대신 원시 `FormData` 객체를 받으려면 액션 정의에서 `input` 속성을 생략하세요.

다음 예시는 사용자의 이메일을 입력받고 "서비스 약관" 동의 체크박스를 요구하는 검증된 뉴스레터 등록 양식을 보여줍니다.

<Steps>

1. 각 입력에 고유한 `name` 속성을 가진 HTML 양식 컴포넌트를 만듭니다:

    ```astro title="src/components/Newsletter.astro" /name="\w+"/
    <form>
      <label for="email">E-mail</label>
      <input id="email" required type="email" name="email" />
      <label>
        <input required type="checkbox" name="terms">
        I agree to the terms of service
      </label>
      <button>Sign up</button>
    </form>
    ```

2. 제출된 양식을 처리할 `newsletter` 액션을 정의합니다. `z.string().email()` 유효성 검사기를 사용하여 `email` 필드의 유효성을 검사하고 `z.boolean()`을 사용하여 `terms` 체크박스의 유효성을 검사합니다:

    ```ts title="src/actions/index.ts" ins={5-12}
    import { defineAction } from 'astro:actions';
    import { z } from 'astro:schema';

    export const server = {
      newsletter: defineAction({
        accept: 'form',
        input: z.object({
          email: z.string().email(),
          terms: z.boolean(),
        }),
        handler: async ({ email, terms }) => { /* ... */ },
      })
    }
    ```

    <ReadMore>사용 가능한 모든 양식 유효성 검사기는 [`input` API 참조](/ko/reference/modules/astro-actions/#input-유효성-검사기)를 확인하세요.</ReadMore>
    
3. HTML 양식에 `<script>`를 추가하여 사용자 입력을 제출합니다. 이 예시에서는 양식의 기본 제출 동작을 재정의하여 `actions.newsletter()`를 호출하고 `navigate()` 함수를 사용하여 `/confirmation`으로 리디렉션합니다:

    ```astro title=src/components/Newsletter.astro ins={11-22} collapse={2-8}
    <form>
      <label for="email">E-mail</label>
      <input id="email" required type="email" name="email" />
      <label>
        <input required type="checkbox" name="terms">
        I agree to the terms of service
      </label>
      <button>Sign up</button>
    </form>

    <script>
      import { actions } from 'astro:actions';
      import { navigate } from 'astro:transitions/client';

      const form = document.querySelector('form');
      form?.addEventListener('submit', async (event) => {
        event.preventDefault();
        const formData = new FormData(form);
        const { error } = await actions.newsletter(formData);
        if (!error) navigate('/confirmation');
      })
    </script>
    ```

    <ReadMore>양식 데이터를 제출하는 다른 방법은 [“HTML 양식 액션에서 액션 호출”](#html-양식-액션에서-액션-호출)을 참조하세요.</ReadMore>
    
</Steps>

### 양식 입력 오류 표시

`required`, `type="email"`, `pattern`과 같은 [기본 HTML 양식 유효성 검사 속성](https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation#using_built-in_form_validation)을 사용하여 제출 전에 양식 입력의 유효성을 검사할 수 있습니다. 백엔드에서 더 복잡한 `input` 유효성 검사를 수행하려면 제공된 [`isInputError()`](/ko/reference/modules/astro-actions/#isinputerror) 유틸리티 함수를 사용할 수 있습니다.

입력 오류를 검색하려면 `isInputError()` 유틸리티를 사용하여 잘못된 입력으로 인해 오류가 발생했는지 확인합니다. 입력 오류에는 유효성 검사에 실패한 각 입력 이름에 대한 메시지가 포함된 `fields` 객체가 포함됩니다. 이러한 메시지를 사용하여 사용자에게 제출물을 수정하라는 메시지를 표시할 수 있습니다.

다음 예시는 `isInputError()`로 오류를 확인하고 이메일 필드에 오류가 있는지 확인한 다음 오류로부터 메시지를 생성합니다. JavaScript DOM 조작 또는 원하는 UI 프레임워크를 사용하여 이 메시지를 사용자에게 표시할 수 있습니다.

```js /isInputError(?= )/ {5-12}
import { actions, isInputError } from 'astro:actions';

const form = document.querySelector('form');
const formData = new FormData(form);
const { error } = await actions.newsletter(formData);
if (isInputError(error)) {
  // 입력 오류를 처리합니다.
  if (error.fields.email) {
    const message = error.fields.email.join(', ');
  }
}
```

## HTML 양식 액션에서 액션 호출

:::note
양식 액션을 사용하여 액션을 호출할 때는 페이지가 주문형으로 렌더링되어야 합니다. 이 API를 사용하기 전에 [페이지에서 사전 렌더링이 비활성화되어 있는지 확인](/ko/guides/on-demand-rendering/#요청-시-렌더링-활성화)하세요.
:::

모든 `<form>` 요소에서 표준 속성을 사용하여 JS 없이 양식 제출을 활성화할 수 있습니다.  클라이언트 측 JavaScript가 없는 양식 제출은 JavaScript가 로드되지 않을 때를 대비하거나 양식을 완전히 서버에서 처리하려는 경우 유용할 수 있습니다.

서버에서 [Astro.getActionResult()](/ko/reference/api-reference/#getactionresult)를 호출하면 양식 제출 결과 (`data` 또는 `error`)가 반환되며, 동적 리디렉션, 양식 오류 처리, UI 업데이트 등에 사용할 수 있습니다.

HTML 양식에서 액션을 호출하려면 `<form>`에 `method="POST"`를 추가한 다음, 액션을 사용하여 양식의 `action` 속성을 설정합니다 (예: `action={actions.logout}`). 이렇게 하면 서버에서 자동으로 처리되는 쿼리 문자열을 사용하도록 `action` 속성이 설정됩니다.

예를 들어, 이 Astro 컴포넌트는 버튼을 클릭하면 `logout` 액션을 호출하고 현재 페이지를 다시 로드합니다:

```astro title="src/components/LogoutButton.astro"
---
import { actions } from 'astro:actions';
---

<form method="POST" action={actions.logout}>
  <button>Log out</button>
</form>
```

Zod가 적절한 스키마 유효성 검사를 수행하기 위해 `<form>` 요소에 속성을 추가해야 할 수도 있습니다. 예를 들어, 파일을 업로드하는 경우에는 `enctype="multipart/form-data"`를 추가하여 `z.instanceof(File)`을 통해 파일이 정확한 형식으로 전송되는지 확인해야 합니다.

```astro title="src/components/FileUploadForm.astro"
---
import { actions } from 'astro:actions';
---
<form method="POST" action={actions.upload} enctype="multipart/form-data" >
  <label for="file">Upload File</label>
  <input type="file" id="file" name="file" />
  <button type="submit">Submit</button>
</form>
```

### 액션 성공 시 리디렉션

성공 시 새로운 라우트로 리디렉션해야 하는 경우, 서버에서 액션의 결과를 사용할 수 있습니다. 일반적인 예시로는 제품 기록을 생성하고 새로운 제품의 페이지(예: `/products/[id]`)로 리디렉션하는 것이 있습니다.

예를 들어 생성된 제품 ID를 반환하는 `createProduct` 액션이 있다고 가정해 보겠습니다:

```ts title="src/actions/index.ts" mark={10}
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';

export const server = {
  createProduct: defineAction({
    accept: 'form',
    input: z.object({ /* ... */ }),
    handler: async (input) => {
      const product = await persistToDatabase(input);
      return { id: product.id };
    },
  })
}
```

Astro 컴포넌트에서 `Astro.getActionResult()`를 호출하여 액션 결과를 검색할 수 있습니다. 액션이 호출되면 `data` 또는 `error` 속성이 포함된 객체를 반환하고, 이 요청 중에 액션이 호출되지 않은 경우 `undefined`를 반환합니다.

`data` 속성을 사용하여 `Astro.redirect()`와 함께 사용할 URL을 구성합니다:

```astro title="src/pages/products/create.astro" {4-7}
---
import { actions } from 'astro:actions';

const result = Astro.getActionResult(actions.createProduct);
if (result && !result.error) {
  return Astro.redirect(`/products/${result.data.id}`);
}
---

<form method="POST" action={actions.createProduct}>
  <!--...-->
</form>
```

### 양식 액션 오류 처리

양식이 포함된 Astro 컴포넌트에서 `Astro.getActionResult()`를 호출하면 사용자 정의 오류 처리를 위해 `data` 및 `error` 객체에 액세스할 수 있습니다.

다음 예시는 `newsletter` 액션이 실패할 때 일반적인 실패 메시지를 표시합니다:

```astro title="src/pages/index.astro" {4,7-9}
---
import { actions } from 'astro:actions';

const result = Astro.getActionResult(actions.newsletter);
---

{result?.error && (
  <p class="error">Unable to sign up. Please try again later.</p>
)}
<form method="POST" action={actions.newsletter}>
  <label>
    E-mail
    <input required type="email" name="email" />
  </label>
  <button>Sign up</button>
</form>
```

더 많은 사용자 지정이 필요한 경우 [`isInputError()` 유틸리티를 사용](#양식-입력-오류-표시)하여 잘못된 입력으로 인해 오류가 발생했는지 확인할 수 있습니다.

다음 예시는 잘못된 이메일이 제출된 경우 `email` 입력 필드 아래에 오류 배너를 렌더링합니다:

```astro title="src/pages/index.astro" ins={5,13} ins='aria-describedby="error"'
---
import { actions, isInputError } from 'astro:actions';

const result = Astro.getActionResult(actions.newsletter);
const inputErrors = isInputError(result?.error) ? result.error.fields : {};
---

<form method="POST" action={actions.newsletter}>
  <label>
    E-mail
    <input required type="email" name="email" aria-describedby="error" />
  </label>
  {inputErrors.email && <p id="error">{inputErrors.email.join(',')}</p>}
  <button>Sign up</button>
</form>
```

#### 오류 시 입력값 유지

입력값은 양식이 제출될 때마다 지워집니다. 입력 값을 유지하려면 [뷰 전환을 활성화](/ko/guides/view-transitions/#뷰-전환-활성화-spa-모드)하고 각 입력에 `transition:persist` 지시문을 적용하면 됩니다:

```astro ins="transition:persist"
<input transition:persist required type="email" name="email" />
```

### 양식 액션 결과로 UI 업데이트

성공 시 사용자에게 알림을 표시하기 위해 액션의 반환 값을 사용하려면, 액션을 `Astro.getActionResult()`에 전달하세요. 반환된 `data` 속성을 사용하여 표시하고자 하는 UI를 렌더링하세요.

이 예시는 `addToCart` 액션이 반환한 `productName` 속성을 사용하여 성공 메시지를 보여줍니다.

```astro title="src/pages/products/[slug].astro"
---
import { actions } from 'astro:actions';

const result = Astro.getActionResult(actions.addToCart);
---

{result && !result.error && (
  <p class="success">Added {result.data.productName} to cart</p>
)}

<!--...-->
```

### 고급: 세션으로 액션 결과 유지하기

<p><Since v="5.0.0" /></p>

액션 결과는 POST 제출로 표시됩니다. 이는 사용자가 페이지를 닫고 다시 방문할 때 결과가 `undefined`로 재설정된다는 것을 의미합니다. 또한 사용자가 페이지를 새로고침하려고 하면 "양식 다시 제출 확인" 대화상자가 표시됩니다.

이 동작을 사용자 정의하기 위해 미들웨어를 추가하여 액션의 결과를 수동으로 처리할 수 있습니다. 쿠키나 세션 스토리지를 사용하여 액션 결과를 유지하도록 선택할 수 있습니다.

[미들웨어 파일을 생성](/ko/guides/middleware/)하고 `astro:actions`에서 [`getActionContext()` 유틸리티](/ko/reference/modules/astro-actions/#getactioncontext)를 가져오는 것부터 시작하세요. 이 함수는 액션 핸들러와 액션이 HTML 양식에서 호출되었는지 여부를 포함한 액션 요청에 대한 정보가 있는 `action` 객체를 반환합니다. `getActionContext()`는 또한 `Astro.getActionResult()`에 의해 반환되는 값을 프로그래밍 방식으로 설정하는 `setActionResult()`와 `serializeActionResult()` 함수를 반환합니다:

```ts title="src/middleware.ts" {2,5}
import { defineMiddleware } from 'astro:middleware';
import { getActionContext } from 'astro:actions';

export const onRequest = defineMiddleware(async (context, next) => {
  const { action, setActionResult, serializeActionResult } = getActionContext(context);
  if (action?.calledFrom === 'form') {
    const result = await action.handler();
    // ... 액션 결과 처리
    setActionResult(action.name, serializeActionResult(result));
  }
  return next();
});
```

HTML 양식 결과를 유지하는 일반적인 방법은 [POST / Redirect / GET 패턴](https://en.wikipedia.org/wiki/Post/Redirect/Get)입니다. 이 리디렉션은 페이지가 새로고침될 때 "양식 다시 제출 확인" 대화상자를 제거하고, 사용자 세션 전체에서 액션 결과가 유지되도록 합니다.

이 예시는 [Netlify 서버 어댑터](/ko/guides/integrations-guide/netlify/)가 설치된 세션 스토리지를 사용하여 모든 양식 제출에 POST / Redirect / GET 패턴을 적용합니다. 액션 결과는 [Netlify Blob](https://docs.netlify.com/blobs/overview/)을 사용하여 세션 스토어에 기록되고, 세션 ID를 사용하여 리디렉션 후에 검색됩니다:

```ts title="src/middleware.ts"
import { defineMiddleware } from 'astro:middleware';
import { getActionContext } from 'astro:actions';
import { randomUUID } from "node:crypto";
import { getStore } from "@netlify/blobs";

export const onRequest = defineMiddleware(async (context, next) => {
  // 사전 렌더링된 페이지에 대한 요청 건너뛰기
  if (context.isPrerendered) return next();
  
  const { action, setActionResult, serializeActionResult } =
    getActionContext(context);
  // Netlify Blob으로 액션 결과를 유지하는 Blob 스토어 만들기
  const actionStore = getStore("action-session");
  
  // 액션 결과가 쿠키로 전달되었다면,
  // `Astro.getActionResult()`에서 접근할 수 있도록 결과를 설정하세요
  const sessionId = context.cookies.get("action-session-id")?.value;
  const session = sessionId
    ? await actionStore.get(sessionId, {
        type: "json",
      })
    : undefined;
  
  if (session) {
    setActionResult(session.actionName, session.actionResult);
  
    // 선택 사항: 페이지가 렌더링된 후 세션을 삭제합니다.
    // 원하는 대로 자신만의 유지 전략을 구현하세요
    await actionStore.delete(sessionId);
    context.cookies.delete("action-session-id");
    return next();
  }
  
  // HTML 양식 액션에서 액션이 호출된 경우, 
  // 액션 핸들러를 호출하고 목적 페이지로 리디렉션합니다
  if (action?.calledFrom === "form") {
    const actionResult = await action.handler();
  
    // 세션 스토리지를 사용하여 액션 결과 유지하기
    const sessionId = randomUUID();
    await actionStore.setJSON(sessionId, {
      actionName: action.name,
      actionResult: serializeActionResult(actionResult),
    });
  
    // 페이지로 리디렉션한 후 검색하기 위해 
    // 세션 ID를 쿠키로 전달
    context.cookies.set("action-session-id", sessionId);
  
    // 오류 발생 시 이전 페이지로 리디렉션
    if (actionResult.error) {
      const referer = context.request.headers.get("Referer");
      if (!referer) {
        throw new Error(
          "Internal: Referer unexpectedly missing from Action POST request.",
        );
      }
      return context.redirect(referer);
    }
    // 성공 시 목적 페이지로 리디렉션
    return context.redirect(context.originPathname);
  }
  
  return next();
});
```

## 액션 사용 시 보안

액션은 액션의 이름을 기반으로 한 공개 엔드포인트로 접근할 수 있습니다. 예를 들어, `blog.like()` 액션은 `/_actions/blog.like`에서 접근할 수 있습니다. 이는 액션 결과의 단위 테스트와 프로덕션 오류 디버깅에 유용합니다. 하지만 이는 API 엔드포인트와 요청 시 렌더링되는 페이지에서 고려해야 할 동일한 인증 검사를 **반드시** 사용해야 한다는 것을 의미합니다.

### 액션 핸들러에서 사용자 인증

액션 요청을 인증하기 위해 액션 핸들러에 인증 검사를 추가하세요. [인증 라이브러리](/ko/guides/authentication/)를 사용하여 세션 관리와 사용자 정보를 처리할 수 있습니다.

액션은 `context.locals`를 사용하여 미들웨어에서 전달된 속성에 접근하기 위한 [`APIContext` 객체의 하위 집합](/ko/reference/modules/astro-actions/#actionapicontext)을 노출합니다. 사용자가 인증되지 않은 경우 `UNAUTHORIZED` 코드와 함께 `ActionError`를 발생시킬 수 있습니다:

```ts title="src/actions/index.ts" {6-8}
import { defineAction, ActionError } from 'astro:actions';

export const server = {
  getUserSettings: defineAction({
    handler: async (_input, context) => {
      if (!context.locals.user) {
        throw new ActionError({ code: 'UNAUTHORIZED' });
      }
      return { /* 성공 시 데이터 */ };
    }
  })
}
```
### 미들웨어에서 액션 제한

<p><Since v="5.0.0" /></p>

Astro는 권한 수준과 액션별 속도 제한을 존중하기 위해 액션 핸들러에서 사용자 세션을 인증하는 것을 권장합니다. 하지만 미들웨어에서 모든 액션(또는 액션의 하위 집합)에 대한 요청을 제한할 수도 있습니다.

미들웨어에서 [`getActionContext()` 함수](/ko/reference/modules/astro-actions/#getactioncontext)를 사용하여 들어오는 액션 요청에 대한 정보를 검색할 수 있습니다. 여기에는 액션 이름과 해당 액션이 클라이언트 측 원격 프로시저 호출(RPC) 함수(예: `actions.blog.like()`) 또는 HTML 양식을 사용하여 호출되었는지 여부가 포함됩니다.

다음 예시는 유효한 세션 토큰이 없는 모든 액션 요청을 거부합니다. 검사가 실패하면 "Forbidden" 응답이 반환됩니다. 참고: 이 방법은 세션이 있을 때만 액션에 접근할 수 있도록 보장하지만, 안전한 인증을 *대체하지는* 않습니다.

```ts title="src/middleware.ts"
import { defineMiddleware } from 'astro:middleware';
import { getActionContext } from 'astro:actions';

export const onRequest = defineMiddleware(async (context, next) => {
  const { action } = getActionContext(context);
  // 액션이 클라이언트 측 함수에서 호출되었는지 확인
  if (action?.calledFrom === 'rpc') {
    // 그렇다면, 사용자 세션 토큰을 확인
    if (!context.cookies.has('user-session')) {
      return new Response('Forbidden', { status: 403 });
    }
  }
  
  context.cookies.set('user-session', /* 세션 토큰 */);
  return next();
});
```

## Astro 컴포넌트 및 서버 엔드포인트에서 액션 호출

Astro 컴포넌트 스크립트에서 `Astro.callAction()` 래퍼 (또는 [서버 엔드포인트](/ko/guides/endpoints/#서버-엔드포인트-api-라우트) 사용 시 `context.callAction()`)를 사용하여 액션을 직접 호출할 수 있습니다. 서버 코드에서 액션의 로직을 재사용하는 것은 일반적인 방법입니다.

첫 번째 인자로 액션을 전달하고 두 번째 인자로 입력 매개변수를 전달합니다. 그러면 클라이언트에서 액션을 호출할 때 받은 것과 동일한 `data` 및 `error` 객체가 반환됩니다:

```astro title="src/pages/products.astro" {6}
---
import { actions } from 'astro:actions';

const searchQuery = Astro.url.searchParams.get('search');
if (searchQuery) {
  const { data, error } = await Astro.callAction(actions.findProduct, { query: searchQuery });
  // 결과 처리
}
---
```
